package org.broadway.jmx;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Date;

import java.util.Map;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.JMX;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadway.interfaces.Context;
import org.broadway.interfaces.ObservedResource;

/**
 * As the name implies, this class provides utilitarian methods
 * used for JMX integration.
 * 
 * @author vladimir vivien
 *
 */
public class JmxUtil {

    private static final Log log = LogFactory.getLog(JmxUtil.class);

    /**
     * The JMX constants are use to store constants values 
     * such as event types.
     * @author vladimir vivien
     */
    public static enum JmxConstants {

        BROADWAY_TIMER_SIGNAL("broadway.timer.signal"),
        BROADWAY_ACTION_SIGNAL("broaday.action.trigger");
        private String listenerType;

        JmxConstants(String type) {
            listenerType = type;
        }

        @Override
        public String toString() {
            return listenerType;
        }
    }

    /**
     * This method returns an instance of an MBeanServer.
     * It first attempts to locate an instance using MBeanServerFactory.
     * If none is found, then uses the ManagementFactory.
     * @return MBeanServer used for component.
     */
    public static MBeanServer getMBeanServer() {
        ArrayList servers = MBeanServerFactory.findMBeanServer(null);
        MBeanServer svr = null;
        if (servers != null && servers.size() > 0) {
            svr = (MBeanServer) servers.get(0);
        } else {
            svr = ManagementFactory.getPlatformMBeanServer();
        }

        if (svr != null) {
            log.debug("Found MBeanServer with default domain [" + svr.getDefaultDomain() + "]");
        } else {
            log.debug("Unable to retrieve a valid MBeanServer.");
        }

        return svr;
    }
    

    /***
     * This method connects to a MBeanServer using a service URL.  It allows 
     * connection to local or remote server via the connector service API. For
     * instance, to connect to a server whose services are exported as RMI stubs
     * you would use service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi
     * @param address - a string specifying the address and protocol for connection in that format.
     * @param env - a Map containing additional environment vars for connector (can be null).
     * @throws JmxException if connection fails.
     * @return MBeanServerConnection instance if all is successful.
     */
    public static MBeanServerConnection getMBeanServer(String address, Map<String,?> env) throws JmxException{
        MBeanServerConnection connection = null;
        try {
            log.info("Connecting to MBeanServer at [" + address + "]");
            JMXConnector connector = JMXConnectorFactory.connect(new JMXServiceURL(address), env);
            connector.connect();
            connection = connector.getMBeanServerConnection();
            log.info("Connected OK to MBeanServer at address [" + address + "]");
        } catch (MalformedURLException ex) {
            throw new JmxException("Unable to get MBeanServerConnection [" + address + "]", ex);
        } catch (IOException ex) {
            throw new JmxException("Unable to get MBeanServerConnection [" + address + "]", ex);
        }
        return connection;
    }
    
    /***
     * This method connects to a MBeanServer using a service URL.  It lets 
     * you connect to local or remote server via the connector service API.
     * @param  address a string representation of address and protocol used to connect to server.
     * @throws JmxException if connection fails.
     * @return MBeanServerConnection instance 
     */
    public static MBeanServerConnection getMBeanServer(String address) throws JmxException{
        return getMBeanServer(address,null);
    }

    /**
     * Use this method to create a JMX ObjectName instance using a 
     * String literal of a jmx id.
     * @param name - a string representation of a jmx name.
     * @return ObjectName instance
     * @throws JmxException this wraps the many exceptions that the JMX API may throw.
     */
    public static ObjectName createObjectName(String name) throws JmxException {
        ObjectName objName = null;

        try {
            objName = ObjectName.getInstance(name);
        } catch (MalformedObjectNameException e) {
            throw new JmxException("Error while creating ObjectName: " + name, e);
        } catch (NullPointerException e) {
            throw new JmxException("Unable to create ObjectName with null value.", e);
        }
        log.debug("Created new ObjectName using jmx name [" + name + "].");
        return objName;
    }
    
    /***
     * This methods creates an MBean proxy object hosted in the specified server.
     * Firstly, it checks to see if the object is registered with the MBean server.
     * If not, it registers the object with the server, then return a proxy version
     * for further operation.
     * @param server - the MBean server where the mbean is registered.
     * @param objName - the Jmx ObjectName for the mbean.
     * @param interfaceClass - the type of the MBean object.
     * @return The a proxied object for the type of the bean specified.
     * @throws org.broadway.jmx.JmxException
     */
    public static <T> T createMBeanProxy(MBeanServerConnection server, ObjectName objName, Class<T> interfaceClass) throws JmxException{
        T proxy;
        try {
            if (!server.isRegistered(objName)) {
                log.debug("MBean not found in MBeanServer, creating new one.");
                // register new bean with server
                server.createMBean(interfaceClass.getName(), objName);
            }
            proxy = JMX.newMBeanProxy(server, objName, interfaceClass);
            log.debug("Created MBean Proxy for object [" + objName + "] of type [" + interfaceClass.getName() + "].");
        } catch (ReflectionException ex) {
            throw new JmxException("Error while creating MBean Proxy: ", ex);
        } catch (InstanceAlreadyExistsException ex) {
            throw new JmxException("Error while creating MBean Proxy: ", ex);
        } catch (MBeanRegistrationException ex) {
            throw new JmxException("Error while creating MBean Proxy: ", ex);
        } catch (MBeanException ex) {
            throw new JmxException("Error while creating MBean Proxy: ", ex);
        } catch (NotCompliantMBeanException ex) {
           throw new JmxException("Error while creating MBean Proxy: ", ex);
        } catch (IOException ex) {
            throw new JmxException("Error while creating MBean Proxy: ", ex);
        }
        return proxy;
    }
    
    
    /**
     * This method provides service to register a bean to an MBeanServer.
     * @param server - the server to which bean will be registered.
     * @param obj - Instnace of the MBean to be registered.
     * @param objName - Instance of ObjectName used to register MBean.
     * @throws JmxException - A wrapper for all JMX API exceptions.
     */
    public static void registerMBean(MBeanServer server, Object obj, ObjectName objName) throws JmxException {
        if (server.isRegistered(objName)) {
            unregisterMBean(server, objName);
        }
        try {
            server.registerMBean(obj, objName);
            log.debug("Registered MBean [" + objName + "] with MBeanServer.");
        } catch (InstanceAlreadyExistsException e) {
            throw new JmxException("Unable to register MBean with name " + objName + ".", e);
        } catch (MBeanRegistrationException e) {
            throw new JmxException("Unable to register MBean with name " + objName + ".", e);
        } catch (NotCompliantMBeanException e) {
            throw new JmxException("Unable to register MBean with name " + objName + ".", e);
        }
    }

    /**
     * This method can be used to unregister an MBean from the provided server.
     * @param server - a local or remote MBean server where MBean is registered.
     * @param objName - name of the MBean to unregister.
     * @throws JmxException - Wrapper exception for any exception thrown by JMX.
     */
    public static void unregisterMBean(MBeanServerConnection server, ObjectName objName) throws JmxException {
        try {
            server.unregisterMBean(objName);
            log.debug("Unregistered MBean [" + objName + "] from MBeanServer.");
        } catch (IOException ex) {
            throw new JmxException("Unable to unregister MBean with name " + objName + ".", ex);
        } catch (InstanceNotFoundException ex) {
            throw new JmxException("Unable to unregister MBean with name " + objName + ".", ex);
        } catch (MBeanRegistrationException ex) {
            throw new JmxException("Unable to unregister MBean with name " + objName + ".", ex);
        }
    }

    /**
     * registerNotificationListener() registers a NotificationListener that listens
     * to events on the JMX event bus.
     * @param server - a local or remote MBeanServer where listerner will be registered.
     * @param broadcaster - the ObjectName of the event broadcaster.
     * @param listener - An intance of the listener which will handle events.
     * @param filter - A filter object used to filter processed events.
     * @param handBack - Any object that can be handed back to the event handler callback method.
     * @throws JmxException - Wrapper to all exceptions that may be thrown.
     */
    public static void registerNotificationListener(MBeanServerConnection server, ObjectName broadcaster, NotificationListener listener, NotificationFilter filter, Object handBack) throws JmxException {
        try {
            server.addNotificationListener(broadcaster, listener, filter, handBack);
            log.debug("Registering notification listener for MBeanServer [" + broadcaster + "].");
        } catch (IOException ex) {
            throw new JmxException("Error registering action listener: ", ex);
        } catch (InstanceNotFoundException ex) {
            throw new JmxException("Error registering action listener: ", ex);
        }
    }

    /**
     * Use this method to register a listener that handle event notifications.
     * @param server - a local or remote MBeanServer where listener will be registered.
     * @param broadcaster - ObjcetName of the event broadcaster.
     * @param listener - ObjectName of the object event handler.
     * @param filter -  a filter object used to filter incoming events.
     * @param handBack - a object instanced passed to the event handler callback method.
     * @throws JmxException - Wrapper of all exceptions may be thrown.
     */
    public static void registerNotificationListener(MBeanServerConnection server, ObjectName broadcaster, ObjectName listener, NotificationFilter filter, Object handBack) throws JmxException {
        try {
            server.addNotificationListener(broadcaster, listener, filter, handBack);
        }catch (IOException ex) {
            throw new JmxException("Error registering action listener: ", ex);
        } catch (InstanceNotFoundException ex) {
            throw new JmxException("Error registering action listener: ", ex);
        }
    }

    /**
     * Registers a notification listener
     * @see {@link #registerNotificationListener(MBeanServer, ObjectName, ObjectName, NotificationFilter, Object)}
     * @param server
     * @param broadcaster
     * @param listener
     * @throws JmxException
     */
    public static void registerNotificationListener(MBeanServerConnection server, ObjectName broadcaster, ObjectName listener) throws JmxException {
        registerNotificationListener(server, broadcaster, listener, null, null);
    }

    /**
     * Remove a registered NotificationListener from the server.
     * @param server
     * @param broadcaster
     * @param listener
     * @throws JmxException
     */
    public static void removeNotificationLitener(MBeanServerConnection server, ObjectName broadcaster, NotificationListener listener) throws JmxException {
        try {
            server.removeNotificationListener(broadcaster, listener);
        }catch (IOException ex) {
            throw new JmxException("Error while removing notification listener - Broadcaster instance not found. ", ex);
        } catch (InstanceNotFoundException ex) {
            throw new JmxException("Error while removing notification listener - Broadcaster instance not found. ", ex);
        } catch (ListenerNotFoundException ex) {
            throw new JmxException("Error while removing notification listener - Listener instance not found. ", ex);
        }
    }

    /**
     * Returns the attribute value from the server for a given MBean.
     * @param server - a local or remote MBeanServer where MBean is registered.
     * @param objectName - ObjectName of MBean.
     * @param attrib - the name of the attribute.
     * @return Object - value of the attribute.
     * @throws JmxException - Wrapper exception of any JMX exception thrown.
     */
    public static Object getAttributeValue(MBeanServerConnection server, ObjectName objectName, String attrib) throws JmxException {
        Object result = null;
        try {
            result = server.getAttribute(objectName, attrib);
        } catch (AttributeNotFoundException e) {
        } catch (InstanceNotFoundException e) {
            throw new JmxException(e);
        } catch (MBeanException e) {
            throw new JmxException(e);
        } catch (ReflectionException e) {
            throw new JmxException(e);
        } catch(IOException ex){
            throw new JmxException(ex);
        }
        return result;

    }

    /**
     * Create an instance of an observed bean object which is a special object
     * used to proxy and monitor registered JMX components.
     * @param server - a local or remote MBeanServer where JMX component is registered.
     * @param name - The JMX object name for the real JMX component.
     * @return ObservedResource
     * @throws JmxException - Wrapper exception for any JMX API exception.
     * @see ObservedResource
     */
    public static ObservedResource createObservedJmxBean(MBeanServerConnection server, ObjectName name) throws JmxException {
        ObservedJmxBean mbean = null;
        try {
            mbean = new ObservedJmxBean(server, name);
        } catch (JmxException e) {
            log.error("Error while creating ObservedJmxBean instance with name [" + name + "]", e);
            throw new JmxException("Error while creating ObservedJmxBean instance with name " + name, e);
        }
        return mbean;
    }

    /**
     * See createObservedJmxMBean()
     * @see {@link #createObservedJmxBean(MBeanServer, ObjectName)}
     * @param server
     * @param jmxName
     * @return
     * @throws JmxException
     */
    public static ObservedResource createObservedJmxBean(MBeanServerConnection server, String jmxName) throws JmxException {
        return createObservedJmxBean(server, createObjectName(jmxName));
    }

    /**
     * This method creates and format a JMX notification object used to send
     * Broadway event notifications.
     * @param context - internal broadway context.
     * @return Notification object
     */
    public static Notification createActionNotification(Context context) {
        Notification note = new Notification(JmxUtil.JmxConstants.BROADWAY_ACTION_SIGNAL.toString(), getNextSequence(), new Date().getTime(), "Broadway Monitor Action Notification");
        note.setUserData(context);
        return note;
    }

    /**
     * Utility method that generates a randomized value.
     * @return long - random value.
     */
    public static long getNextSequence() {
        return new Date().getTime() + new java.util.Random().nextLong();
    }
}
